Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Win32: Spawn extension processes in job #7838

Merged
merged 3 commits into from
Dec 11, 2024

Conversation

mook-as
Copy link
Contributor

@mook-as mook-as commented Nov 30, 2024

This puts any processes spawned by the extension in a job, so that they can be terminated when the main application exits.

We intentionally let the main Rancher Desktop process have a handle to the new job (which isn't ever referenced), and set the job up so that it will automatically terminate when all handles to that job are closed. This means that as the main process exits, it will automatically cause all processes in that job to terminate (because it would have the only outstanding handle to the job).

For #7653

This puts any processes spawned by the extension in a job, so that they
can be terminated when the main application exits.

Signed-off-by: Mark Yen <mark.yen@suse.com>
When we fail to open a process (which we'll do a lot because we shouldn't
be able to affect processes under the system account), only show a message
in debug logging instead of all the time.

Signed-off-by: Mark Yen <mark.yen@suse.com>
}
type JOBOBJECT_EXTENDED_LIMIT_INFORMATION struct {
BasicLimitInformation JOBOBJECT_BASIC_LIMIT_INFORMATION
IoInfo struct {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit; can you put the IoInfo outside as you did with JOBOBJECT_BASIC_LIMIT_INFORMATION so it's easier on the eyes when looking at the reference?

heapFree = hKernel32.NewProc("HeapFree")
)

// Convert a list of arguments into a command line for use with CreateProcess.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// Convert a list of arguments into a command line for use with CreateProcess.
// buildCommandLine converts a slice of arguments into a properly formatted command line string
// suitable for use with the CreateProcess API. This function is the reverse of
// `windows.DecomposeCommandLine()`, which parses a command line string into individual arguments.
//
// The function follows the parsing rules for command-line arguments as outlined in:
// https://learn.microsoft.com/en-us/cpp/c-language/parsing-c-command-line-arguments
//
// Key behaviors include:
// - The first argument (typically the executable name) is treated specially and enclosed in double quotes
// without applying backslash escape rules, including for embedded quotes.
// - Each subsequent argument is wrapped in double quotes, and any internal quotes or backslashes are
// escaped appropriately according to the rules for Windows command-line parsing:
// - Backslashes preceding a quote are doubled (e.g., `\\` becomes `\\\\`), and the quote itself is escaped.
// - Backslashes followed by non-quote characters are preserved as-is.
// - The function ensures that the final command line is properly formatted for passing to `CreateProcess`,
// handling edge cases such as arguments containing spaces, quotes, or backslashes.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Mostly taken, but with some tweaks (and checking with godoc to ensure the HTML rendering is correct).

func buildCommandLine(args []string) string {
// See https://learn.microsoft.com/en-us/cpp/c-language/parsing-c-command-line-arguments
// for details on how it must be parsed.
var result []byte
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think for better performance when dealing with string concatenation (especially when appending repeatedly in a loop) it makes sense to use string builder instead since it is designed for string concatenation operations.
Also, you are calling append so many times in a loop which could be inefficient.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looking at strings.Builder, that's actually even more awkward (because the WriteString API says it must always return a nil error which we must then check (because errcheck). I guess it's slightly more efficient in that the buffer it allocates is not zeroed, and it grabs the data directly when returning the string? I'll use it then.

// We need the handle to have a stable address for the jobs list; we
// do this by allocating memory in C to avoid the golang GC moving
// things around.
heap, _, err := getProcessHeap.Call(0, 0, 0)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it should be fine to call getProcessHeap.Call() without any args.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is; I have no idea why I decided 0, 0, 0 was useful.


jobList, _, err := heapAlloc.Call(heap, 0, unsafe.Sizeof(job))
if jobList == 0 {
return nil, fmt.Errorf("failed to allocate memory: %s", err)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
return nil, fmt.Errorf("failed to allocate memory: %s", err)
return nil, fmt.Errorf("failed to allocate memory: %w", err)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks; also couple attribute list errors below.

defer func() {
_ = windows.CloseHandle(job)
}()
if !errors.Is(err, os.ErrExist) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if !errors.Is(err, os.ErrExist) {
// Check if the job was newly created or already exists
if !errors.Is(err, os.ErrExist) {


// Set the job so processes can't break away, and it terminates when the
// last handle is closed.
var limits JOBOBJECT_EXTENDED_LIMIT_INFORMATION
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code organization suggestion:

// configureJobLimits sets the job limits to prevent breakaway processes and ensures
// the job terminates when the last handle is closed.
func configureJobLimits(job windows.Handle) error {
    var limits JOBOBJECT_EXTENDED_LIMIT_INFORMATION
    ok, _, err := queryInformationJobObject.Call(
        uintptr(job),
        JobObjectExtendedLimitInformation,
        uintptr(unsafe.Pointer(&limits)),
        unsafe.Sizeof(limits),
        uintptr(unsafe.Pointer(nil)))
    if ok == 0 {
        return fmt.Errorf("error looking up job limits: %w", err)
    }

    // Modify job limits to prevent breakaway and ensure termination on job close
    limits.BasicLimitInformation.LimitFlags &= ^(JOB_OBJECT_LIMIT_BREAKAWAY_OK | JOB_OBJECT_LIMIT_SILENT_BREAKAWAY_OK)
    limits.BasicLimitInformation.LimitFlags |= JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE

    ok, _, err = setInformationJobObject.Call(
        uintptr(job),
        JobObjectExtendedLimitInformation,
        uintptr(unsafe.Pointer(&limits)),
        unsafe.Sizeof(limits))
    if ok == 0 {
        return fmt.Errorf("error setting job limits: %w", err)
    }

    return nil
}

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add a helper function? Sure, I'll do that.


// Duplicate the job into the given process (but leaking it). This way when
// the target process exits, it will shut down the job.
hProc, err := windows.OpenProcess(windows.PROCESS_DUP_HANDLE, false, pid)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also,

// injectJobHandleIntoParent duplicates the job handle into the given parent process
// so the parent can manage the job's lifecycle.
func injectJobHandleIntoParent(pid uint32, job windows.Handle) error {
    hProc, err := windows.OpenProcess(windows.PROCESS_DUP_HANDLE, false, pid)
    if err != nil {
        return fmt.Errorf("failed to open parent process %d: %w", pid, err)
    }
    defer windows.CloseHandle(hProc)

    err = windows.DuplicateHandle(windows.CurrentProcess(), job, hProc, nil, 0, false, 0)
    if err != nil {
        return fmt.Errorf("failed to duplicate job handle: %w", err)
    }

    return nil
}

t.Parallel()
cases := [][]string{
{"arg0", "a b c", "d", "e"},
{"C:\\Program Files\\arg0\\\\", "ab\"c", "\\", "d"},
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should there also be a test for mixed forward and backward slashes or is that too crazy?
e.g C:/Program Files\\Test

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added test. This also happened to catch an error when there are no arguments :)


func TestBuildCommandLine(t *testing.T) {
t.Parallel()
cases := [][]string{
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are some more cases that I can think of, but I will leave it up to you to see if they make sense or not:

  • Spaces in the middle of the argument
  • Trailing spaces (extra spaces after quotes, etc)
  • Windows-specific characters that are valid in the command line and have special meaning e.g. &, |, >, <, etc.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, I'll add those. (The ones here are mostly from the documentation.) Though the first one is covered with de fg.

Signed-off-by: Mark Yen <mark.yen@suse.com>
@mook-as mook-as requested a review from Nino-K December 11, 2024 21:52
@mook-as mook-as merged commit aca6a4d into rancher-sandbox:main Dec 11, 2024
21 checks passed
@mook-as mook-as deleted the factory-reset/use-jobs branch December 11, 2024 22:54
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants